/*
 * Copyright © 2009 Gerald Isaac Schwarz
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package base;

import java.awt.Point;
import java.util.ArrayList;

import javax.swing.event.EventListenerList;

public class Engine {
	private Board board;
	private EventListenerList listenerList = new EventListenerList();
	private ArrayList<Piece> pieces = new ArrayList<Piece>();
	
	public Board getBoard() {
		return board;
	}
	
	public Engine(Board board) {
		this.board = board;
	}
	
	public boolean move(Piece piece, Point destination) {
		State state = this.getBoard().getCurrentState(piece);
		
		if (state.getOwner() == this.getBoard().getCurrentPlayer() && this.isValidMove(piece, destination)) {
			if (!this.getBoard().isEmpty(destination)) {
				Piece otherPiece = this.getBoard().getPiece(destination);
				State otherState = this.getBoard().getCurrentState(otherPiece);
				
				if (otherState.getOwner() == state.getOwner()) {
					this.getBoard().setLevel(piece, otherState.getLevel() + state.getLevel());
				}
				else if (otherState.getLevel() >= state.getLevel()) {
						this.getBoard().setLevel(piece, otherState.getLevel() + 1);
				}
				
				this.getBoard().remove(otherPiece);
			}
			this.getBoard().setCurrentPlayer(this.getBoard().getNextPlayer());
			this.getBoard().setLocation(piece, destination);
			
			this.removeAllPieces();
			this.notifyTurnListeners(new TurnEvent(this));
			return true;
		}
		else {
			return false;
		}
	}
	
	public boolean isValidMove(Piece piece, Point destination) {
		return destination.equals(piece.getMoves()[destination.y][destination.x]);
	}
	
	private Point[][] getPossibleMoves(Point origin, int remainigMoves) {
		int ringCount = (remainigMoves / 2) + (remainigMoves % 2);
		Point[][] targets = new Point[this.getBoard().getSize().height][this.getBoard().getSize().width];
		
		Point[] north = new Point[ringCount];
		Point[] east = new Point[ringCount];
		Point[] south = new Point[ringCount];
		Point[] west = new Point[ringCount];
		
		for (int i = ringCount - 1; i >= 0; i--) {
			north[i] = new Point(origin.x, (origin.y - remainigMoves) + (i * 2));
			south[i] = new Point(origin.x, (origin.y + remainigMoves) - (i * 2));
			east[i] = new Point((origin.x + remainigMoves) - (i * 2), origin.y);
			west[i] = new Point((origin.x - remainigMoves) + (i * 2), origin.y);
		}
		
		for (int i = 0; i < ringCount ; i++) {
			Point northPoint = north[i];
			Point southPoint = south[i];
			Point eastPoint = east[i];
			Point westPoint = west[i];
			
			Point addition = northPoint;
			if (this.getBoard().contains(addition)) {
				targets[addition.y][addition.x] = addition;
			}
			while(!eastPoint.equals(addition)) {
				addition = new Point(addition);
				addition.translate(1, 1);
				if (this.getBoard().contains(addition)) {
					targets[addition.y][addition.x] = addition;
				}
			}
			
			addition = eastPoint;
			if (this.getBoard().contains(addition)) {
				targets[addition.y][addition.x] = addition;
			}
			while(!southPoint.equals(addition)) {
				addition = new Point(addition);
				addition.translate(-1, 1);
				if (this.getBoard().contains(addition)) {
					targets[addition.y][addition.x] = addition;
				}
			}
			
			addition = southPoint;
			if (this.getBoard().contains(addition)) {
				targets[addition.y][addition.x] = addition;
			}
			while(!westPoint.equals(addition)) {
				addition = new Point(addition);
				addition.translate(-1, -1);
				if (this.getBoard().contains(addition)) {
					targets[addition.y][addition.x] = addition;
				}
			}
			
			addition = westPoint;
			if (this.getBoard().contains(addition)) {
				targets[addition.y][addition.x] = addition;
			}
			while(!northPoint.equals(addition)) {
				addition = new Point(addition);
				addition.translate(1, -1);
				if (this.getBoard().contains(addition)) {
					targets[addition.y][addition.x] = addition;
				}
			}
		}
		
		return targets;
	}
	
	private Point[][] filterUnreachableTargets(Point origin, int remainingMoves, Point[][] targets) {
		Point[][] points = new Point[this.getBoard().getSize().height][this.getBoard().getSize().width];
		
		for (int y = 0; y < targets.length; y++) {
			for (int x = 0; x < targets[y].length; x++) {
				if (targets[y][x] != null) {
					ArrayList<Point> path = this.getPath(origin, targets[y][x], remainingMoves, new ArrayList<Point>());
					
					if (path.size() > 0 && path.get(path.size() - 1).equals(targets[y][x])) {
						points[y][x] = targets[y][x];
					}
				}
			}
		}
		
		return points;
	}
	
	private ArrayList<Point> getPath(Point origin, Point destination, int remainingMoves, ArrayList<Point> path) {
		remainingMoves--;
		
		ArrayList<Point> locations = new ArrayList<Point>();
		locations.add(new Point(origin.x, origin.y + 1));
		locations.add(new Point(origin.x + 1, origin.y));
		locations.add(new Point(origin.x, origin.y - 1));
		locations.add(new Point(origin.x - 1, origin.y));
		
		for (Point location : locations) {
			int distance = Math.abs(destination.x - location.x) + Math.abs(destination.y - location.y);
			if (remainingMoves > 0 && this.getBoard().contains(location) && this.getBoard().isEmpty(location) && 
					!this.hasPoint(path, location) && distance <= remainingMoves && !this.hasPoint(path, destination)) {
				ArrayList<Point> path2 = new ArrayList<Point>(path);
				path2.add(location);
				ArrayList<Point> path3 = this.getPath(location, destination, remainingMoves, path2);
				if (path3.get(path3.size() - 1).equals(destination)) {
					return path3;
				}
			}
			else if (remainingMoves == 0 && location.equals(destination) && !this.hasPoint(path, location)) {
				path.add(location);
			}
		}
		
		return path;
	}
	
	private boolean hasPoint(ArrayList<Point> points, Point point) {
		for (Point point2 : points) {
			if (point.equals(point2)) {
				return true;
			}
		}
		
		return false;
	}
	
	private void addPiece(Piece piece) {
		piece.setRefreshMoves(false);
		this.pieces.add(piece);
	}
	
	private void removeAllPieces() {
		for (Piece piece : this.pieces) {
			piece.setRefreshMoves(true);
		}
		
		this.pieces = new ArrayList<Piece>();
	}
	
	public Point[][] getMoves(Piece piece) {
		if (piece.isRefreshMoves()) {
			Point[][] targets = new Point[this.getBoard().getSize().height][this.getBoard().getSize().width];
			State currentState = this.getBoard().getCurrentState(piece);
			int remainingMoves = currentState.getLevel();
			targets = this.getPossibleMoves(currentState.getLocation(), remainingMoves);
			targets = this.filterUnreachableTargets(currentState.getLocation(), remainingMoves, targets);
			
			piece.setMoves(targets);
			this.addPiece(piece);
		}
		
		return piece.getMoves();
	}
	
	public void addTurnListener(TurnListener turnListener) {
		this.listenerList.add(TurnListener.class, turnListener);
	}
	
	private void notifyTurnListeners(TurnEvent turnEvent) {
		Object[] listeners = listenerList.getListenerList();
		
		for (int i = 0; i < listeners.length; i += 2) {
			if (listeners[i] == TurnListener.class) {
            	((TurnListener)listeners[i + 1]).turnPerformed(turnEvent);
            }
        }
	}
}
